/**
 * Copyright (c) 2015 Bosch Software Innovations GmbH and others
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.hawkbit.repository.jpa.tenancy;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;

import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.test.util.DisposableSqlTestDatabaseExtension;
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;

/**
 * Multi-Tenancy tests which testing the CRUD operations of entities that all CRUD-Operations are tenant aware and cannot access
 * or delete entities not belonging to the current tenant.
 * <p/>
 * Feature: Component Tests - Repository<br/>
 * Story: Multi Tenancy
 */
@ExtendWith(DisposableSqlTestDatabaseExtension.class)
class MultiTenancyEntityTest extends AbstractJpaIntegrationTest {

    /**
     * Ensures that multiple targets with same controller-ID can be created for different tenants.
     */
    @Test
    void createMultipleTargetsWithSameIdForDifferentTenant() throws Exception {
        // known controller ID for overall tenants same
        final String knownControllerId = "controllerId";

        // known tenant names
        final String tenant = "aTenant";
        final String anotherTenant = "anotherTenant";
        // create targets
        createTargetForTenant(knownControllerId, tenant);
        createTargetForTenant(knownControllerId, anotherTenant);

        // ensure both tenants see their target
        final Page<? extends Target> findTargetsForTenant = findTargetsForTenant(tenant);
        assertThat(findTargetsForTenant).hasSize(1);
        assertThat(findTargetsForTenant.getContent().get(0).getTenant().toUpperCase()).isEqualTo(tenant.toUpperCase());
        final Page<? extends Target> findTargetsForAnotherTenant = findTargetsForTenant(anotherTenant);
        assertThat(findTargetsForAnotherTenant).hasSize(1);
        assertThat(findTargetsForAnotherTenant.getContent().get(0).getTenant().toUpperCase())
                .isEqualTo(anotherTenant.toUpperCase());
    }

    /**
     * Ensures that targets created by a tenant are not visible by another tenant.
     */
    @Test
    @WithUser(tenantId = "mytenant", allSpPermissions = true)
    void queryTargetFromDifferentTenantIsNotVisible() throws Exception {
        // create target for another tenant
        final String anotherTenant = "anotherTenant";
        final String controllerAnotherTenant = "anotherController";
        createTargetForTenant(controllerAnotherTenant, anotherTenant);

        // find all targets for current tenant "mytenant"
        final Page<? extends Target> findTargetsAll = targetManagement.findAll(PAGE);
        // no target has been created for "mytenant"
        assertThat(findTargetsAll).isEmpty();

        // find all targets for anotherTenant
        final Page<? extends Target> findTargetsForTenant = findTargetsForTenant(anotherTenant);
        // another tenant should have targets
        assertThat(findTargetsForTenant).hasSize(1);
    }

    /**
     * Ensures that tenant with proper permissions can read and delete other tenants.
     */
    @Test
    @WithUser(tenantId = "mytenant", allSpPermissions = true)
    void deleteAnotherTenantPossible() throws Exception {
        // create target for another tenant
        final String anotherTenant = "anotherTenant";
        final String controllerAnotherTenant = "anotherController";
        createTargetForTenant(controllerAnotherTenant, anotherTenant);

        assertThat(systemManagement.findTenants(PAGE)).as("Expected number if tenants before deletion is").hasSize(3);

        systemManagement.deleteTenant(anotherTenant);

        assertThat(systemManagement.findTenants(PAGE)).as("Expected number if tenants after deletion is").hasSize(2);
    }

    /**
     * Ensures that tenant metadata is retrieved for the current tenant.
     */
    @Test
    @WithUser(tenantId = "mytenant", autoCreateTenant = false, allSpPermissions = true)
    void getTenantMetdata() throws Exception {
        // logged in tenant mytenant - check if tenant default data is
        // autogenerated
        assertThat(distributionSetTypeManagement.findAll(PAGE)).isEmpty();
        SecurityContextSwitch.callAsPrivileged(() ->
                assertThat(systemManagement.createTenantMetadata("mytenant").getTenant().toUpperCase()).isEqualTo("mytenant".toUpperCase()));

        assertThat(distributionSetTypeManagement.findAll(PAGE)).isNotEmpty();

        // check that the cache is not getting in the way, i.e. "bumlux" results in bumlux and not mytenant
        assertThat(SecurityContextSwitch.getAs(
                SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", "bumlux"),
                () -> systemManagement.getTenantMetadataWithoutDetails().getTenant().toUpperCase()))
                .isEqualTo("bumlux".toUpperCase());
    }

    /**
     * Ensures that targets created from a different tenant cannot be deleted from other tenants
     */
    @Test
    @WithUser(tenantId = "mytenant", allSpPermissions = true)
    void deleteTargetFromOtherTenantIsNotPossible() throws Exception {
        // create target for another tenant
        final String anotherTenant = "anotherTenant";
        final String controllerAnotherTenant = "anotherController";
        final List<Long> createTargetForTenant = List.of(createTargetForTenant(controllerAnotherTenant, anotherTenant).getId());

        // ensure target cannot be deleted by 'mytenant'
        try {
            targetManagement.delete(createTargetForTenant);
            fail("mytenant should not have been able to delete target of anotherTenant");
        } catch (final EntityNotFoundException ex) {
            // ok
        }

        Page<? extends Target> targetsForAnotherTenant = findTargetsForTenant(anotherTenant);
        assertThat(targetsForAnotherTenant).hasSize(1);

        // ensure another tenant can delete the target
        deleteTargetsForTenant(anotherTenant, createTargetForTenant);
        targetsForAnotherTenant = findTargetsForTenant(anotherTenant);
        assertThat(targetsForAnotherTenant).isEmpty();
    }

    /**
     * Ensures that multiple distribution sets with same name and version can be created for different tenants.
     */
    @Test
    void createMultipleDistributionSetsWithSameNameForDifferentTenants() throws Exception {

        // known tenant names
        final String tenant = "aTenant";
        final String anotherTenant = "anotherTenant";
        // create distribution sets
        createDistributionSetForTenant(tenant);
        createDistributionSetForTenant(anotherTenant);

        // ensure both tenants see their distribution sets
        final Slice<? extends DistributionSet> findDistributionSetsForTenant = findDistributionSetForTenant(tenant);
        assertThat(findDistributionSetsForTenant).hasSize(1);
        assertThat(findDistributionSetsForTenant.getContent().get(0).getTenant().toUpperCase())
                .isEqualTo(tenant.toUpperCase());
        final Slice<? extends DistributionSet> findDistributionSetsForAnotherTenant = findDistributionSetForTenant(anotherTenant);
        assertThat(findDistributionSetsForAnotherTenant).hasSize(1);
        assertThat(findDistributionSetsForAnotherTenant.getContent().get(0).getTenant().toUpperCase())
                .isEqualTo(anotherTenant.toUpperCase());
    }

    private <T> T runAsTenant(final String tenant, final Callable<T> callable) throws Exception {
        return SecurityContextSwitch.callAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable);
    }

    private Target createTargetForTenant(final String controllerId, final String tenant) throws Exception {
        return runAsTenant(tenant, () -> testdataFactory.createTarget(controllerId));
    }

    private Page<? extends Target> findTargetsForTenant(final String tenant) throws Exception {
        return runAsTenant(tenant, () -> targetManagement.findAll(PAGE));
    }

    private void deleteTargetsForTenant(final String tenant, final Collection<Long> targetIds) throws Exception {
        runAsTenant(tenant, () -> {
            targetManagement.delete(targetIds);
            return null;
        });
    }

    private void createDistributionSetForTenant(final String tenant) throws Exception {
        runAsTenant(tenant, () -> testdataFactory.createDistributionSet());
    }

    private Slice<? extends DistributionSet> findDistributionSetForTenant(final String tenant) throws Exception {
        return runAsTenant(tenant, () -> distributionSetManagement.findAll(PAGE));
    }
}